package oneD.t3dviewer2;

/* ThreeD.java  A set of classes to parse, represent and display
                tensegrity models -- see comments at
                Model3D.load(InputStream) for description of data file format

   The colors for negative transformed z-values are lighter.

   History:

   12 Jun 08  Do not handle exceptions in load(), pass them on.
              Remove ThreeDClient:  messages probably are not useful
              anymore and should be done with subscribe pattern anyway.
              Do not close the InputStream in load().  Merge all
              reset functions into resetTransform().  Add reset() which
              puts things back in initial state and redraws.  Do not
              report model errors by drawing into display area.
              In paintBuffer() clear the buffer before testing for empty
              model and possibly exiting.
   10 Jun 08  Extract Model3D class and rename it CompactModel.
   05 Jun 08  Delete "token" variable for use with StreamTokenizer.  Not
              needed since value is stored in a publicly accessible field.
   13 Jul 06  Change name of paint() to paintBuffer() and typ to type.
              Fix comments.  Print a stack trace for all exceptions.
   12 Jul 06  Move all transformation and rendering from Model3D to ThreeD.
              Remove repaint(0) from updateBuffer() and call it explicitly
              when necessary.  Remove some useless synchronization.
   12 Jun 06  Remove doUpdate arg from setMagnification() and needless
              java.awt.event.* import.
   06 Jun 06  Split the color of the lines that straddle the z == 0 plane.
   01 Jun 06  Delete incrementMagnification() and incrementShift().
              Eliminate unused z component for shift.
              Add doUpdate arg to setMagnification().
              Have resetShift() just call setShift().
              Add resetMagnification().  Adjust
              rotation magnification by the model magnification
              (slow it down as model gets bigger).
   31 May 06  Rename startRotation as startDrag.  Add updateShift and
              resetShift.
   30 May 06  Change name of findBB to findBoundingSphere.  Use float
              constant instead of casting double constant to float.
              Change name of updateMagnification to incrementMagnification
              and let the client set maximimum and minimum values for
              magnification.  Add facilities for setting a shift value.
   29 May 06  Take advantage of new arot() in matrix3d to implement drag
              rotation.  m_rotUpdate no longer needed.
   26 May 06  Add support for drag rotation.
   25 May 06  Use bounding sphere instead of bounding box.  Rename
              m_bufferImage as m_graphicsBuffer.  Stub rotation for now.
   21 Sep 05  Replace deprecated API constructs.
   20 Sep 05  Remove import directive for ThreeDClient.  Apparently
              not necessary, and javac 1.4.2 doesn't like it.
	      Use static initialization for Model3D.m_colors[].
   06 Sep 05  In the initial comment for this file, indicate where
              a description of the data file format can be found below.
   22 Jul 05  Remove user messages regarding buffer update.  Machines
              are fast enough now this probably slows things down more
              than it helps cope with a slow machine.  Add a message
              to indicate that model data loaded successfully.
   24 Mar 99  Add javadoc comments.  Get rid of unused zmid() member
              functions.  Make updateBuffer more bullet proof.
   24 Feb 99  Embed ThreeDClient interface in ThreeD class.  Qualify
              several functions with "synchronized".
   06 Oct 98  Improve modularity:  abstract ThreeDClient provides messaging
              capabilities; ThreeD.load() works from InputStream rather
              than URL.  Modify to allow response to new Canvas size.
   26 Aug 98  Make initial magnification and rotation explicitly setable.
              ThreeD.load() no longer initializes these parameters.  Make
              interfaces to rotation and magnification comparable:
              set... sets parameter values without triggering redraw;
              update... increments parameter values and triggers redraw.
              Redid paint() and update() logic so file format exceptions
              displayed.  Privatized Model3D items that ThreeD doesn't use.
   04 Aug 98  Raise initial rotation about x-axis from 20 to 200 degrees.
   08 Jul 98  Add double buffering.

   To do:

   Generate faded colors by averaging in background color rather than White.
 */

import java.awt.*;
import java.io.*;

/**
 * Manage the way an instance of CompactModel is drawn on Canvas. Much of the
 * rendering technique and geometry management came originally from the JDK
 * 1.1.1 wireframe example.
 * 
 * @author Bob Burkhardt
 * @see CompactModel
 */
public class ThreeD extends Canvas {
	/**
	 * The wireframe model.
	 */
	CompactModel m_md;
	/**
	 * Array of indices into the m_md's line array so lines can be accessed by
	 * z-value of midpoints.
	 */
	private int m_lines[];
	/**
	 * Where all graphics are rendered. This needs to be maintained at the same
	 * size as the Canvas and is drawn onto the Canvas any time the image needs
	 * updating.
	 */
	Image m_graphicsBuffer;
	/**
	 * Graphics context used for rendering the graphics.
	 * 
	 * @see #m_graphicsBuffer
	 */
	Graphics m_gc;
	/**
	 * Width of the graphics buffer (needs to match that of Canvas).
	 */
	int m_width;
	/**
	 * Height of the graphics buffer (needs to match that of Canvas).
	 */
	int m_height;
	/**
	 * Scale value which multiplies model coordinates to get the pixel
	 * coordinates and z values used to render the wireframe on Canvas.
	 */
	float m_scale;
	/**
	 * Margin adjustment for the scale value so there's a little space around
	 * the model at magnification 1.0.
	 */
	final static float MARGIN_SCALE_ADJUST = 0.96f;
	/**
	 * Transformation matrix applied to the model coordinates to get device
	 * coordinates.
	 */
	matrix3d m_mat = new matrix3d();
	/**
	 * Transformed coordinates for all model points (pixel locations + z value)
	 * 
	 * @see matrix3d
	 */
	int m_tverts[];
	/**
	 * z-value (doubled for convenience) of midpoint for each transformed line.
	 */
	int m_zmids[];
	/**
	 * Additional magnification value applied on top of m_scale. Ranges from 1.0
	 * to MAX_VIEW_MAGNIFICATION.
	 * 
	 * @see #m_scale
	 */
	float m_mag;
	/**
	 * Adjustment so dragging moves the front of the figure by more than the
	 * length of the drag by the given adjustment factor.
	 */
	final static float ROTATION_MAGNIFICATION_FACTOR = 3.0f;
	/**
	 * Rotation matrix which determines the orientation of the model display.
	 * Used by updateBuffer as one component for m_mat, the model's
	 * transformation matrix.
	 * 
	 * @see #updateBuffer
	 * @see #m_mat
	 */
	matrix3d m_rot = new matrix3d();
	/**
	 * Matrix to store shift factor which is an offset applied to the location
	 * of the model after any rotation.
	 * 
	 * @see #updateBuffer
	 * @see #m_mat
	 */
	matrix3d m_shift = new matrix3d();
	/**
	 * X coordinate of current shift vector. Ranges from -m_md.m_radius to
	 * m_md.m_radius. Applied after rotation and before magnification.
	 */
	float m_shiftX;
	/**
	 * Y coordinate of current shift vector. Ranges from -m_md.m_radius to
	 * m_md.m_radius. Applied after rotation and before magnification.
	 */
	float m_shiftY;
	/**
	 * Coordinates of last observed mouse position (only updated during drags).
	 */
	int m_x = 0, m_y = 0;
	/**
	 * Dimension of largest square fitting on the Canvas.
	 */
	int m_dim = 0;
	/**
	 * Colors used by paintBuffer to render the lines. Lines in the back half of
	 * the figure are rendered with faded versions of the colors used to render
	 * the lines in the front half of the figure.
	 * 
	 * @see paintBuffer
	 */
	private static final Color m_colors[] = new Color[8];

	/**
	 * Create an intance of this GUI component.
	 */
	public ThreeD() {
		m_md = new CompactModel();
		resetTransform();
	}

	static {
		m_colors[0] = new Color(255, 140, 0);
		m_colors[1] = new Color(0, 0, 0);
		m_colors[2] = new Color(255, 0, 0);
		m_colors[3] = new Color(0, 128, 0);
		m_colors[4] = new Color(255, 192, 128);
		m_colors[5] = new Color(128, 128, 128);
		m_colors[6] = new Color(255, 128, 128);
		m_colors[7] = new Color(96, 192, 96);
	}

	/**
	 * Initialize the model geometry from data contained in a stream. Set
	 * scaling.
	 * 
	 * @param is
	 *            stream of data compatible with Model3D format
	 * @exception IOException
	 *                Input problem with the stream of model data.
	 * @exception Model3D.FileFormatException
	 *                Format problem with the stream.
	 * @exception Model3D.ModelException
	 *                Problem storing model in Model3D.
	 */
	public void load(InputStream is) throws IOException,
			Model3D.FileFormatException, Model3D.ModelException {
		resetTransform();
		m_md.load(is);
		float f1 = getWidth() / (2.0f * m_md.m_radius);
		float f2 = getHeight() / (2.0f * m_md.m_radius);
		m_scale = MARGIN_SCALE_ADJUST * ((f1 < f2) ? f1 : f2);
		updateBuffer();
		repaint(0);
	}

	/**
	 * Set initial mouse coordinates for start of mouse-drag rotation or shift.
	 * 
	 * @param x
	 *            x coordinate of mouse
	 * @param y
	 *            y coordinate of mouse
	 */
	public void startDrag(int x, int y) {
		m_x = x;
		m_y = y;
	}

	/**
	 * Increment the rotation depending on new mouse coordinates.
	 * 
	 * @param x
	 *            x coordinate of mouse
	 * @param y
	 *            y coordinate of mouse
	 */
	public void updateRotation(int x, int y) {
		if (x == m_x && y == m_y)
			return;
		float deltax = 2.0f * (x - m_x) / m_dim;
		float deltay = 2.0f * (y - m_y) / m_dim;
		m_x = x;
		m_y = y;

		// angle of rotation (radians)
		double angle = (float) Math.sqrt(deltax * deltax + deltay * deltay);
		// axis of rotation (normalized to length == 1; z == 0)
		float axis_x = (float) (-deltay / angle);
		float axis_y = (float) (deltax / angle);
		angle *= (180.0 / Math.PI) * ROTATION_MAGNIFICATION_FACTOR / m_mag;

		// update rotation matrix
		m_rot.arot(angle, axis_x, axis_y, 0.0f);
		updateBuffer();
		repaint(0);
	}

	/**
	 * Reset transform to initial state.
	 */
	private void resetTransform() {
		// rotation
		m_rot.unit();
		m_rot.zrot(180.0); // so y-axis points up instead of down

		// shift
		setShift(0.0f, 0.0f);

		// magnification
		m_mag = 1.0f;
	}

	/**
	 * Reset transform and model to initial state.
	 */
	public void reset() {
		resetTransform();
		m_md.reset();
		updateBuffer();
		repaint(0);
	}

	/**
	 * Set the magnification value.
	 * 
	 * @param mag
	 *            magnification value (1.0 is no magnification; negative values
	 *            will result in reflection)
	 */
	public void setMagnification(float value) {
		m_mag = value;
		updateBuffer();
		repaint(0);
	}

	/**
	 * Set the shift vector. Applied after rotation, but before magnification.
	 * 
	 * @param x
	 *            x component of shift vector
	 * @param y
	 *            y component of shift vector (each clipped to between
	 *            -m_md.m_radius and m_md.m_radius)
	 */
	public void setShift(float x, float y) {
		if (x < -m_md.m_radius)
			x = -m_md.m_radius;
		else if (x > m_md.m_radius)
			x = m_md.m_radius;
		if (y < -m_md.m_radius)
			y = -m_md.m_radius;
		else if (y > m_md.m_radius)
			y = m_md.m_radius;
		m_shiftX = x;
		m_shiftY = y;
		m_shift.unit();
		m_shift.translate(x, y, 0.0f);
	}

	/**
	 * Update the shift depending on new mouse coordinates and the current
	 * magnification.
	 * 
	 * @param x
	 *            x coordinate of mouse
	 * @param y
	 *            y coordinate of mouse
	 */
	public void updateShift(int x, int y) {
		if (x == m_x && y == m_y)
			return;

		float shiftx = (x - m_x) / (m_scale * m_mag) + m_shiftX;
		float shifty = (y - m_y) / (m_scale * m_mag) + m_shiftY;
		m_x = x;
		m_y = y;

		setShift(shiftx, shifty);
		updateBuffer();
		repaint(0);
	}

	/**
	 * Redraw m_graphicsBuffer according to current model geometry and
	 * transformations. This function basically sets things up. Most of the
	 * actual rendering is done by paintBuffer().
	 * 
	 * @see #m_graphicsBuffer
	 * @see #paintBuffer
	 */
	public void updateBuffer() {
		if (m_md != null) {
			m_mat.unit();

			// center about the origin
			m_mat.translate(-m_md.m_xmean, -m_md.m_ymean, -m_md.m_zmean);

			// rotate about the origin
			m_mat.mult(m_rot);

			// apply shift
			m_mat.mult(m_shift);

			// do reality check on graphics buffer dimensions
			if (m_graphicsBuffer == null || m_width != getWidth()
					|| m_height != getHeight()) {
				m_width = getWidth();
				m_height = getHeight();
				m_dim = m_width < m_height ? m_width : m_height;
				m_graphicsBuffer = createImage(m_width, m_height);
				if (m_graphicsBuffer != null)
					m_gc = m_graphicsBuffer.getGraphics();
				m_scale = MARGIN_SCALE_ADJUST * m_dim / (2.0f * m_md.m_radius);
			}

			if (m_graphicsBuffer != null && m_gc == null)
				m_gc = m_graphicsBuffer.getGraphics();

			// scale graphics according to magnification and viewport
			m_mat.scale(m_scale * m_mag, m_scale * m_mag, m_scale * m_mag);

			// move graphics to center of viewport
			m_mat.translate(m_width / 2, m_height / 2, 0);

			// clear buffer and draw graphics in it
			if (m_graphicsBuffer != null && m_gc != null)
				paintBuffer();
		}
	}

	/**
	 * Heap sort m_zmids array into increasing order. Adapted from S. Dvorak and
	 * A. Musset, BASIC in Action, p. 170. Transforms to m_zmids array are
	 * mirrored in m_lines array.
	 */
	private synchronized void sort() {
		int i;
		int shift_index, zmid_value, line_value;

		// Organize array into descending heap (largest value at root)
		for (i = (m_md.nlines() - 1) / 2; i >= 0; i--)
			shift_up(m_zmids[i], m_lines[i], i, m_md.nlines());

		/*
		 * Create array sorted in increasing order (starting from end). As for
		 * loop progresses, array gets filled from the end, and the remainder of
		 * the array is maintained as a descending heap which steadily becomes
		 * smaller. v[0] always contains the largest value of the remaining
		 * heap.
		 */
		for (i = m_md.nlines() - 1; i > 0; i--) {
			/*
			 * grab value at and index of current array position being filled
			 * (it will be inserted into remaining heap)
			 */
			zmid_value = m_zmids[i];
			line_value = m_lines[i];
			// put largest value of remaining heap in final position
			m_zmids[i] = m_zmids[0];
			m_lines[i] = m_lines[0];
			// shift value up into remaining descending heap
			shift_up(zmid_value, line_value, 0, i);
		}
	}

	/**
	 * Shift zmid_value up into heap until it's not less than successors.
	 * Transforms to m_zmids array are mirrored in m_lines array.
	 * 
	 * @see #sort
	 */
	private void shift_up(int zmid_value, int line_value, int start_index,
			int max_index) {
		int j; // initially index of "left" successor of v[start_index]

		while ((j = start_index + start_index + 1) < max_index) {
			// grab "right" successor of m_zmids[start_index] if it has larger
			// value
			if (j + 1 < max_index && m_zmids[j] < m_zmids[j + 1])
				j++;
			if (zmid_value < m_zmids[j]) // shift value less than a successor
			{
				// put this successor in shift_value's old position
				m_zmids[start_index] = m_zmids[j];
				m_lines[start_index] = m_lines[j];
				// consider shift_value to now occupy successor's position
				start_index = j;
			} else
				break; /* shift_value not less than successors */
		}
		// put value in place
		m_zmids[start_index] = zmid_value;
		m_lines[start_index] = line_value;
	}

	/**
	 * Transform all the points in this model using m_mat.
	 * 
	 * @see #m_mat
	 * @see matrix3d
	 */
	private void transform() {
		if (m_md.nverts() <= 0)
			return;
		if (m_tverts == null || m_tverts.length < m_md.nverts() * 3)
			m_tverts = new int[m_md.nverts() * 3];
		m_mat.transform(m_md.verts(), m_tverts, m_md.nverts());
	}

	/**
	 * Render m_md in m_graphicsBuffer. m_mat is used to map from model space to
	 * screen space. The colors for lines with negative transformed midpoint
	 * z-values are lighter than those with positive z-values.
	 */
	synchronized void paintBuffer() {
		m_gc.setColor(Color.lightGray);
		m_gc.fillRect(0, 0, m_width, m_height);
		if (m_md.nlines() <= 0 || m_md.nverts() <= 0)
			return;
		transform();
		if (m_lines == null || m_lines.length < m_md.nlines()) {
			m_lines = new int[m_md.nlines()];
			m_zmids = new int[m_md.nlines()];
		}
		for (int i = 0; i < m_md.nlines(); i++) {
			m_zmids[i] = m_tverts[m_md.p1(i) + 2] + m_tverts[m_md.p2(i) + 2];
			m_lines[i] = i;
		}
		sort();
		for (int i = 0; i < m_md.nlines(); i++) {
			int p1 = m_md.p1(m_lines[i]);
			int p2 = m_md.p2(m_lines[i]);
			int type = m_md.type(m_lines[i]);

			// untransformed z values for endpoints
			int z1 = m_tverts[p1 + 2];
			int z2 = m_tverts[p2 + 2];
			if (z1 >= 0 && z2 >= 0) {
				m_gc.setColor(m_colors[type]);
				m_gc.drawLine(m_tverts[p1], m_tverts[p1 + 1], m_tverts[p2],
						m_tverts[p2 + 1]);
			} else if (z1 < 0 && z2 < 0) {
				m_gc.setColor(m_colors[type + 4]);
				m_gc.drawLine(m_tverts[p1], m_tverts[p1 + 1], m_tverts[p2],
						m_tverts[p2 + 1]);
			} else { // line straddles z == 0 plane; draw it in two colors
				int x1 = m_tverts[p1];
				int x2 = m_tverts[p2];
				int y1 = m_tverts[p1 + 1];
				int y2 = m_tverts[p2 + 1];
				if (z2 < z1) { // make it so the first point is behind the z ==
								// 0 plane
					int itemp;
					itemp = x1;
					x1 = x2;
					x2 = itemp;
					itemp = y1;
					y1 = y2;
					y2 = itemp;
					itemp = z1;
					z1 = z2;
					z2 = itemp;
				}
				int zdiff = z2 - z1;
				int xsplit = z2 * (x1 - x2) / zdiff;
				int ysplit = z2 * (y1 - y2) / zdiff;
				m_gc.setColor(m_colors[type + 4]);
				m_gc.drawLine(x2 + xsplit, y2 + ysplit, x1, y1);
				m_gc.setColor(m_colors[type]);
				m_gc.drawLine(x2, y2, x2 + xsplit, y2 + ysplit);
			}
		}
	}

	public void update(Graphics g) {
		if (m_md != null) {
			if (m_graphicsBuffer == null || m_width != getWidth()
					|| m_height != getHeight())
				updateBuffer();
			g.drawImage(m_graphicsBuffer, 0, 0, this);
		}
	}

	public void paint(Graphics g) {
		update(g);
	}

}
